package org.yaac.server.egql.evaluator;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.Lists.newLinkedList;
import static com.google.common.collect.Sets.newHashSet;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.yaac.server.egql.EntityProperty;
import org.yaac.server.egql.evaluator.function.BlobFunction;
import org.yaac.server.egql.evaluator.function.BlobKeyFunction;
import org.yaac.server.egql.evaluator.function.CategoryFunction;
import org.yaac.server.egql.evaluator.function.DatetimeFunction;
import org.yaac.server.egql.evaluator.function.DoubleFunction;
import org.yaac.server.egql.evaluator.function.EmailFunction;
import org.yaac.server.egql.evaluator.function.FormatFunction;
import org.yaac.server.egql.evaluator.function.Function;
import org.yaac.server.egql.evaluator.function.GeoPtFunction;
import org.yaac.server.egql.evaluator.function.IMHandleFunction;
import org.yaac.server.egql.evaluator.function.KeyFunction;
import org.yaac.server.egql.evaluator.function.LCaseFunction;
import org.yaac.server.egql.evaluator.function.LenFunction;
import org.yaac.server.egql.evaluator.function.LinkFunction;
import org.yaac.server.egql.evaluator.function.ListFunction;
import org.yaac.server.egql.evaluator.function.LongFunction;
import org.yaac.server.egql.evaluator.function.MidFunction;
import org.yaac.server.egql.evaluator.function.NowFunction;
import org.yaac.server.egql.evaluator.function.NullFunction;
import org.yaac.server.egql.evaluator.function.PhoneNumberFunction;
import org.yaac.server.egql.evaluator.function.PostalAddressFunction;
import org.yaac.server.egql.evaluator.function.RoundFunction;
import org.yaac.server.egql.evaluator.function.ShortBlobFunction;
import org.yaac.server.egql.evaluator.function.TextFunction;
import org.yaac.server.egql.evaluator.function.TypeFunction;
import org.yaac.server.egql.evaluator.function.UCaseFunction;
import org.yaac.server.egql.evaluator.function.UserFunction;
import org.yaac.server.egql.exception.EGQLException;
import org.yaac.server.egql.processor.ProcessData.ProcessDataRecord;



/**
 * @author Max Zhu (thebbsky@gmail.com)
 *
 */
public class FunctionEvaluator extends Evaluator {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	private enum Type {
		UCASE(UCaseFunction.class), 
		LCASE(LCaseFunction.class), 
		MID(MidFunction.class), 
		LEN(LenFunction.class), 
		ROUND(RoundFunction.class), 
		FORMAT(FormatFunction.class), 
		NOW(NowFunction.class), 
		TYPE(TypeFunction.class), 
		// used to convert types
		DATETIME(DatetimeFunction.class), 
		KEY(KeyFunction.class), 
		USER(UserFunction.class), 
		GEOPT(GeoPtFunction.class), 
		NULL(NullFunction.class), 	
		LONG(LongFunction.class), 	// supposed to be used in update / insert statements only
		DOUBLE(DoubleFunction.class), 	// supposed to be used in update / insert statements only
		BLOBKEY(BlobKeyFunction.class),
		BLOB(BlobFunction.class),
		TEXT(TextFunction.class),
		IMHANDLE(IMHandleFunction.class),
		LIST(ListFunction.class),
		SHORTBLOB(ShortBlobFunction.class),
		POSTALADDRESS(PostalAddressFunction.class),
		PHONENUMBER(PhoneNumberFunction.class),
		LINK(LinkFunction.class),
		EMAIL(EmailFunction.class),
		CATEGORY(CategoryFunction.class);
		
		Class<? extends Function> clazz;
		
		private Type(Class<? extends Function> clazz) {
			this.clazz = clazz;
		}
		
		public Function newFunctionInstance() {
			try {
				return clazz.newInstance();
			} catch (InstantiationException e) {
				// should not happen
				e.printStackTrace();
				return null;
			} catch (IllegalAccessException e) {
				// should not happen
				e.printStackTrace();
				return null;
			}
		}
	}
	
	private final List<Evaluator> ops;
	
	private Type type;
		  
	/**
	 * @param type
	 */
	public FunctionEvaluator(String type) {
		checkArgument(!isNullOrEmpty(type), "Function name is mandatory");
		
		for (Type t : Type.values()) {
			if (t.toString().equals(type.toUpperCase())) {
				this.type = t;
				break;
			}
		}
		
		checkNotNull(this.type, "Invalid aggregation name : " + type);
		
		this.ops = newLinkedList();
	}

	/**
	 * @param op
	 * @return
	 */
	public FunctionEvaluator add(Evaluator op) {
		ops.add(op);
		return this;
	}

	private Function function;
	
	private Function getFunction() {
		if (function == null) {
			function = this.type.newFunctionInstance();
			function.setOps(ops);
		}
		
		return function;
	}
	
	@Override
	public EvaluationResult evaluate(ProcessDataRecord record) {
		return getFunction().evaluate(record).withTitle(this.getText());
	}
	
	@Override
	public void validate() throws EGQLException {
		this.getFunction().validate();
	}
	
	@Override
	public Set<AggregationEvaluator> aggregationChildren() {
		Set<AggregationEvaluator> result = newHashSet();
		for (Evaluator op : ops) {
			result.addAll(op.aggregationChildren());
		}
		return result;
	}
	
	@Override
	public Set<EntityProperty> nonAggregationProperties() {
		Set<EntityProperty> result = new HashSet<EntityProperty>();
		for (Evaluator op : this.ops) {
			result.addAll(op.nonAggregationProperties());
		}
		return result;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = super.hashCode();
		result = prime * result + ((ops == null) ? 0 : ops.hashCode());
		result = prime * result + ((type == null) ? 0 : type.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (!super.equals(obj))
			return false;
		if (getClass() != obj.getClass())
			return false;
		FunctionEvaluator other = (FunctionEvaluator) obj;
		if (ops == null) {
			if (other.ops != null)
				return false;
		} else if (!ops.equals(other.ops))
			return false;
		if (type != other.type)
			return false;
		return true;
	}

	@Override
	public String toString() {
		return "FunctionEvaluator [ops=" + ops + ", type=" + type + "]";
	}
}